/*
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.iec.caf.data.jpa.repository;

import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess;
import org.hibernate.engine.jdbc.spi.JdbcCoordinator;
import org.hibernate.internal.SessionImpl;

import javax.persistence.EntityManager;
import java.sql.*;

/**
 * 包装entityManager，以更好地支持批量插入等
 * 注意：该类受事务控制，但是必须保证构造的EntityManager是在事务begin之后获取传入的。
 *       因为EntityManager在事务里获取才能取得受事务控制的em，事务外获取的和事务里获取的不是同一个对象
 * @author wangyandong
 * @date 2021/07/19 17:03
 *
 */
public class CafEntityManagerWrapper implements AutoCloseable {
    private EntityManager em;
    private boolean useOuterSession=false;
    private Connection conn;

    /**
     * 构造函数
     * @param em
     */
    public CafEntityManagerWrapper(EntityManager em){
        this.em = em;
    }


    /**
     * Creates a <code>PreparedStatement</code> object for sending
     * parameterized SQL statements to the database.
     * @param sql an SQL statement that may contain one or more '?' IN
     * parameter placeholders
     * @return a new default <code>PreparedStatement</code> object containing the
     * pre-compiled SQL statement
     * @exception SQLException if a database access error occurs
     * or this method is called on a closed connection
     */
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        this.ensureOpen();
        return this.conn.prepareStatement(sql);
    }

    /**
     * Creates a <code>PreparedStatement</code> object that will generate
     * <code>ResultSet</code> objects with the given type, concurrency,
     * and holdability.
     * <P>
     * This method is the same as the <code>prepareStatement</code> method
     * above, but it allows the default result set
     * type, concurrency, and holdability to be overridden.
     *
     * @param sql a <code>String</code> object that is the SQL statement to
     *            be sent to the database; may contain one or more '?' IN
     *            parameters
     * @param resultSetType one of the following <code>ResultSet</code>
     *        constants:
     *         <code>ResultSet.TYPE_FORWARD_ONLY</code>,
     *         <code>ResultSet.TYPE_SCROLL_INSENSITIVE</code>, or
     *         <code>ResultSet.TYPE_SCROLL_SENSITIVE</code>
     * @param resultSetConcurrency one of the following <code>ResultSet</code>
     *        constants:
     *         <code>ResultSet.CONCUR_READ_ONLY</code> or
     *         <code>ResultSet.CONCUR_UPDATABLE</code>
     * @param resultSetHoldability one of the following <code>ResultSet</code>
     *        constants:
     *         <code>ResultSet.HOLD_CURSORS_OVER_COMMIT</code> or
     *         <code>ResultSet.CLOSE_CURSORS_AT_COMMIT</code>
     * @return a new <code>PreparedStatement</code> object, containing the
     *         pre-compiled SQL statement, that will generate
     *         <code>ResultSet</code> objects with the given type,
     *         concurrency, and holdability
     * @exception SQLException if a database access error occurs, this
     * method is called on a closed connection
     *            or the given parameters are not <code>ResultSet</code>
     *            constants indicating type, concurrency, and holdability
     * @exception SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method or this method is not supported for the specified result
     * set type, result set holdability and result set concurrency.
     * @see ResultSet
     * @since 1.4
     */
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        this.ensureOpen();
        return this.conn.prepareStatement(sql,resultSetType,resultSetConcurrency,resultSetHoldability);
    }

    public PreparedStatement prepareStatement(String sql, int columnIndexes[]) throws SQLException {
        this.ensureOpen();
        return this.conn.prepareStatement(sql,columnIndexes);
    }

    public PreparedStatement prepareStatement(String sql, String columnNames[]) throws SQLException {
        this.ensureOpen();
        return this.conn.prepareStatement(sql,columnNames);
    }

    /**
     * Closes this resource, relinquishing any underlying resources.
     * This method is invoked automatically on objects managed by the
     * {@code try}-with-resources statement.
     * @throws Exception if this resource cannot be closed
     */
    @Override
    public void close() throws Exception {
        //使用自身session，且连接未关闭的，则主动关闭连接
        if (this.useOuterSession ==false && this.conn != null && !this.conn.isClosed()) {
            this.conn.close();
        }

        this.conn = null;
        this.em = null;
    }

    //关闭statement和connection
    public void closeAll(Statement statement) throws Exception {
        if(statement!=null && !statement.isClosed()){
            statement.close();
        }
        this.close();
    }

    /**
     * 确保连接是打开的
     */
    private void ensureOpen(){
        try {
            if(this.conn!=null && !this.conn.isClosed())
                return;

            SessionImpl session = (SessionImpl) this.em.getDelegate();
            //如果session已经打开，则直接获取当前连接
            if (session.isOpen() && session.isConnected()) {
                this.useOuterSession = true;
                JdbcCoordinator jdbcConnectionAccess = session.getJdbcCoordinator();
                this.conn = jdbcConnectionAccess.getLogicalConnection().getPhysicalConnection();
            } else {
                //外层无事务，直接构造一个
                this.useOuterSession = false;

                JdbcConnectionAccess jdbcConnectionAccess = session.getJdbcConnectionAccess();
                this.conn = jdbcConnectionAccess.obtainConnection();
            }
        }catch (Exception ex){
            throw new RuntimeException(ex.getMessage(),ex);
        }
    }
}
